package db

import (
	"context"
	"errors"
	"fmt"
	"runtime/debug"
	"time"

	"v8s/compose/model"
	"v8s/logger"
	resourcename "v8s/resource-name"

	"v8s-controller/internal"
	"v8s-controller/internal/configuration"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"

	"go.uber.org/zap"
)

type (
	ProjectsRepository struct {
		log *zap.SugaredLogger

		client     *MongoClient
		properties configuration.MongoProperties
	}

	ProjectResult struct {
		Project *Project
		Error   error
	}

	Project struct {
		DocumentID primitive.ObjectID `json:"-" bson:"_id,omitempty"`
		ID         string             `json:"project-id" bson:"project-id"`
		LockedAt   *time.Time         `json:"collector-locked-at,omitempty" bson:"collector-locked-at,omitempty"`
		SeenAt     *time.Time         `json:"collector-seen-at,omitempty" bson:"collector-seen-at,omitempty"`
		Tasks      []Task             `json:"tasks" bson:"tasks"`
		Cred       LoggingCredentials `json:"logging-credentials,omitempty" bson:"logging-credentials,omitempty"`
	}

	Task struct {
		ID            internal.TaskID                `json:"task-id" bson:"task-id"`
		RN            resourcename.ResourceName      `json:"resource-name" bson:"resource-name"`
		TN            TaskName                       `json:"task-name" bson:"task-name"`
		Status        TaskStatus                     `json:"status" bson:"status"`
		LogStatus     LoggingStatus                  `json:"logging-status,omitempty" bson:"logging-status,omitempty"`
		ModifiedAt    time.Time                      `bson:"modified-at"`
		StatusDetails *string                        `json:"status-details,omitempty" bson:"status-details"`
		Compose       *model.Compose                 `json:"compose,omitempty" bson:"compose,omitempty"`
		Clusters      map[ClusterName]ClusterDetails `json:"clusters" bson:"clusters"`
	}

	ClusterName string
	TaskName    string

	ClusterDetails struct {
		Status    ClusterStatus               `json:"status" bson:"status"`
		Resources []Resource                  `json:"resources" bson:"resources"`
		Services  map[ServiceName]ServiceInfo `json:"services" bson:"services"`
	}

	Resource struct {
		Kind   string         `json:"kind" bson:"kind"`
		Name   string         `json:"name" bson:"name"`
		Status ResourceStatus `json:"status" bson:"status"`
	}

	ServiceName    string
	ResourceStatus string

	TaskStatus string

	LoggingStatus string

	ClusterStatus string

	LoggingCredentials struct {
		User     string `json:"user" bson:"user,omitempty"`
		Password string `json:"password" bson:"password,omitempty"`
	}

	ServiceInfo struct {
		Endpoints []string `bson:"endpoints,omitempty"`
	}

	IdentifiableProject struct {
		DocumentID primitive.ObjectID `bson:"_id,omitempty"`
		ID         string             `bson:"project-id"`
	}

	IdentifiableTask struct {
		ID internal.TaskID `json:"task-id" bson:"task-id"`
	}

	decoder interface {
		Decode(into interface{}) error
	}

	TaskWithStatuses struct {
		ID       internal.TaskID                `json:"task-id" bson:"task-id"`
		RN       resourcename.ResourceName      `json:"resource-name" bson:"resource-name"`
		Status   TaskStatus                     `json:"status" bson:"status"`
		Clusters map[ClusterName]ClusterDetails `json:"clusters" bson:"clusters"`
	}
)

const (
	ProjectsCollection = "projects"
	lockTTL            = -1 * time.Minute

	loggingSeenAtProjectProperty   = "logging-collector-seen-at"
	loggingLockedAtProjectProperty = "logging-collector-locked-at"
	seenAtProjectProperty          = "collector-seen-at"
	lockedAtProjectProperty        = "collector-locked-at"
	modifiedAtTaskProperty         = "modified-at"
	statusTaskProperty             = "status"
	loggingStatusTaskProperty      = "logging-status"
	statusDetailTaskProperty       = "status-detail"

	CreationRequestTaskStatus   = "creation-request"
	RunningTaskStatus           = "running"
	DeletionRequestTaskStatus   = "deletion-request"
	DeletedTaskStatus           = "deleted"
	SuspensionRequestTaskStatus = "suspension-request"
	SuspendedTaskStatus         = "suspended"
	ResumeRequestTaskStatus     = "resume-request"
	UpdateRequestTaskStatus     = "update-request"
	ErrorTaskStatus             = "error"

	ClusterStatusStarting ClusterStatus = "starting"
	ClusterStatusRunning  ClusterStatus = "running"
	ClusterStatusStopped  ClusterStatus = "stopped"
	ClusterStatusStopping ClusterStatus = "stopping"
	ClusterStatusError    ClusterStatus = "error"
	ClusterStatusDeleting ClusterStatus = "deleting"

	ResourceStatusPending   ResourceStatus = "Pending"
	ResourceStatusError     ResourceStatus = "Error"
	ResourceStatusRunning   ResourceStatus = "Running"
	ResourceStatusFailed    ResourceStatus = "Failed"
	ResourceStatusUnknown   ResourceStatus = "Unknown"
	ResourceStatusSucceeded ResourceStatus = "Succeeded"
	ResourceStatusBound     ResourceStatus = "Bound"
	ResourceStatusLost      ResourceStatus = "Lost"

	CreationRequestLoggingStatus = "creation-request"
	UpdateRequstLoggingStatus    = "patch-request"
	SuccessLoggingStatus         = "success"
	ErrorLoggingStatus           = "error"
)

func CreateProjectsRepository(client *MongoClient, properties configuration.MongoProperties) *ProjectsRepository {
	repository := &ProjectsRepository{
		log: logger.Logger().Named("db-projects-repository").Sugar(),

		client:     client,
		properties: properties,
	}

	err := repository.init()
	if err != nil {
		panic(fmt.Errorf("failed to init repository: %w", err))
	}

	return repository
}

func (repository *ProjectsRepository) init() error {
	//names, err := repository.client.
	//	Database(repository.properties.Database).
	//	Collection(projectsCollection).
	//	Indexes().
	//	CreateMany(context.TODO(), []mongo.IndexModel{
	//		mongo.IndexModel{
	//			Keys:    bson.D{},
	//			Options: nil,
	//		},
	//	})
	//
	//return err
	return nil
}

func (repository *ProjectsRepository) ListProjectsWithAllTaskStatusesNotInSince(ctx context.Context, limit int, notAcceptedStatuses []TaskStatus, point time.Time) <-chan ProjectResult {
	// Хотя бы одна задача в проекте должна быть не приостановлена и не удалена
	taskFilter := bson.M{"status": bson.M{"$not": bson.M{"$in": notAcceptedStatuses}}}
	acceptedTaskStatuses := bson.M{"tasks": bson.M{"$elemMatch": taskFilter}}

	// Чтобы задача не попадала бесконечное количество раз в один и тот же запуск поиска
	notSeen := or(
		bson.M{seenAtProjectProperty: bson.M{"$exists": false}},
		bson.M{seenAtProjectProperty: bson.M{"$lt": primitive.NewDateTimeFromTime(point)}})

	return repository.createChannel(limit, func() *ProjectResult {
		// Для паралельной работы нескольких инстансов контроллера:
		// 1. Задача должна быть разлочена
		// 2. Время лока истекло
		lockFree := or(
			bson.M{lockedAtProjectProperty: bson.M{"$exists": false}},
			bson.M{lockedAtProjectProperty: bson.M{"$lt": primitive.NewDateTimeFromTime(time.Now().Add(lockTTL))}})

		where := and(acceptedTaskStatuses, lockFree, notSeen)
		update := set(bson.M{lockedAtProjectProperty: primitive.NewDateTimeFromTime(time.Now())})

		result := repository.collection().FindOneAndUpdate(ctx, where, update)

		if errors.Is(result.Err(), mongo.ErrNoDocuments) {
			return nil
		}

		if result.Err() != nil {
			return &ProjectResult{Error: fmt.Errorf("error happend while quiering: %w", result.Err())}
		}

		project := &Project{}
		if projectError := safeConvert(result, project); projectError != nil {
			// possible only one/several of tasks has error with decoding

			idProject := &IdentifiableProject{}
			if err := safeConvert(result, idProject); err != nil {
				// we can not handle project at all, so returning global error
				return &ProjectResult{Error: fmt.Errorf("failed to recover identifiable project: %s: %w", err, projectError)}
			}

			cursor, err := repository.collection().Aggregate(ctx, mongo.Pipeline{
				{{Key: "$match", Value: bson.M{"_id": idProject.DocumentID}}},
				// selecting task individually
				{{Key: "$unwind", Value: "$tasks"}},
				// flat-mapping tasks
				{{Key: "$replaceRoot", Value: bson.M{"newRoot": "$tasks"}}},
				// getting only not ignored tasks
				{{Key: "$match", Value: taskFilter}},
			})
			if err != nil {
				// we can not handle project at all, so returning global error
				return &ProjectResult{Error: fmt.Errorf("failed to aggregate tasks in project '%s': %s: %w", idProject.DocumentID, err, projectError)}
			}

			defer func() {
				err = cursor.Close(ctx)
				if err != nil {
					repository.log.Errorf("failed to close cursor: %s", err)
				}
			}()

			if cursor.Err() != nil {
				return &ProjectResult{Error: fmt.Errorf("cursor had error without calling next: %w", cursor.Err())}
			}

			tasks := make([]Task, 0)

			for cursor.Next(ctx) {
				task := Task{}

				if errors.Is(cursor.Err(), mongo.ErrNoDocuments) {
					// no documents in cursor, breaking
					break
				}

				if taskErr := safeConvert(cursor, &task); taskErr != nil {
					// we have found broken task, so we must try to change it status to error

					idTask := &IdentifiableTask{}
					if err := safeConvert(cursor, idTask); err != nil {
						// we can't extract id, so just log and skip...
						repository.log.Errorf("failed to decode identifiable task in project '%s': %s: %s", idProject.DocumentID, err, taskErr)
						continue
					}

					details := fmt.Sprintf("failed to decode task: %s", taskErr)

					brokenUpdate := set(
						bson.M{"tasks.$[broken].status": ErrorTaskStatus},
						bson.M{"tasks.$[broken].status-detail": details},
						bson.M{"tasks.$[broken].modified-at": primitive.NewDateTimeFromTime(time.Now())},
					)

					brokenTaskFilters := options.Update().SetArrayFilters(options.ArrayFilters{Filters: []interface{}{
						bson.M{"broken.task-id": idTask.ID},
					}})

					if err := repository.updateByID(ctx, idProject.DocumentID, brokenUpdate, brokenTaskFilters); err != nil {
						repository.log.Errorf("failed to update broken task '%s': %s", idTask.ID, err)
						continue
					}

					repository.log.Errorf("saved broken task '%s' with status error: %s", idTask.ID, details)
					continue
				}

				tasks = append(tasks, task)
			}

			return &ProjectResult{Project: &Project{DocumentID: idProject.DocumentID, ID: idProject.ID, Tasks: tasks}}
		}

		return &ProjectResult{Project: project}
	})
}

func (repository *ProjectsRepository) ListTasksWithStatuses(ctx context.Context, limit int) (chan TaskWithStatuses, error) {
	cursor, err := repository.collection().Aggregate(ctx, mongo.Pipeline{
		// flat-mapping tasks
		{{Key: "$unwind", Value: "$tasks"}},
		// flat-mapping tasks
		{{Key: "$replaceRoot", Value: bson.M{"newRoot": "$tasks"}}},
	})
	if err != nil {
		return nil, fmt.Errorf("failed to aggregate tasks: %s", err)
	}

	results := make(chan TaskWithStatuses, limit)

	go func() {
		defer func() {
			close(results)

			err := cursor.Close(ctx)
			if err != nil {
				repository.log.Errorf("failed to close cursor, while listing task statuses: %s", err)
			}
		}()

		for cursor.Next(ctx) {
			task := TaskWithStatuses{}

			if errors.Is(cursor.Err(), mongo.ErrNoDocuments) {
				// no documents in cursor, breaking
				break
			}

			if err := safeConvert(cursor, &task); err != nil {
				repository.log.Errorf("failed to decode task with status: %s", err)
				continue
			}

			results <- task
		}
	}()

	return results, nil
}

func (repository *ProjectsRepository) ListProjectsWithAllLogsTaskStatusesNotInSince(ctx context.Context, limit int, notAcceptedStatuses []LoggingStatus, point time.Time) <-chan ProjectResult {
	// Хотя бы одна задача в проекте должна быть не приостановлена и не удалена
	taskFilter := bson.M{"logging-status": bson.M{"$not": bson.M{"$in": notAcceptedStatuses}}}
	acceptedTaskStatuses := bson.M{"tasks": bson.M{"$elemMatch": taskFilter}}

	// Чтобы задача не попадала бесконечное количество раз в один и тот же запуск поиска
	notSeen := or(
		bson.M{loggingSeenAtProjectProperty: bson.M{"$exists": false}},
		bson.M{loggingSeenAtProjectProperty: bson.M{"$lt": primitive.NewDateTimeFromTime(point)}})

	return repository.createChannel(limit, func() *ProjectResult {
		// Для паралельной работы нескольких инстансов контроллера:
		// 1. Задача должна быть разлочена
		// 2. Время лока истекло
		lockFree := or(
			bson.M{loggingLockedAtProjectProperty: bson.M{"$exists": false}},
			bson.M{loggingLockedAtProjectProperty: bson.M{"$lt": primitive.NewDateTimeFromTime(time.Now().Add(lockTTL))}})

		where := and(acceptedTaskStatuses, lockFree, notSeen)
		update := set(bson.M{loggingLockedAtProjectProperty: primitive.NewDateTimeFromTime(time.Now())})

		result := repository.collection().FindOneAndUpdate(ctx, where, update)

		if errors.Is(result.Err(), mongo.ErrNoDocuments) {
			return nil
		}

		if result.Err() != nil {
			return &ProjectResult{Error: fmt.Errorf("error happend while quiering: %w", result.Err())}
		}

		project := &Project{}
		if projectError := safeConvert(result, project); projectError != nil {
			// possible only one/several of tasks has error with decoding

			idProject := &IdentifiableProject{}
			if err := safeConvert(result, idProject); err != nil {
				// we can not handle project at all, so returning global error
				return &ProjectResult{Error: fmt.Errorf("failed to recover identifiable project: %s: %w", err, projectError)}
			}

			cursor, err := repository.collection().Aggregate(ctx, mongo.Pipeline{
				{{Key: "$match", Value: bson.M{"_id": idProject.DocumentID}}},
				// selecting task individually
				{{Key: "$unwind", Value: "$tasks"}},
				// flat-mapping tasks
				{{Key: "$replaceRoot", Value: bson.M{"newRoot": "$tasks"}}},
				// getting only not ignored tasks
				{{Key: "$match", Value: taskFilter}},
			})
			if err != nil {
				// we can not handle project at all, so returning global error
				return &ProjectResult{Error: fmt.Errorf("failed to aggregate tasks in project '%s': %s: %w", idProject.DocumentID, err, projectError)}
			}

			defer func() {
				err = cursor.Close(ctx)
				if err != nil {
					repository.log.Errorf("failed to close cursor: %s", err)
				}
			}()

			if cursor.Err() != nil {
				return &ProjectResult{Error: fmt.Errorf("cursor had error without calling next: %w", cursor.Err())}
			}

			tasks := make([]Task, 0)

			for cursor.Next(ctx) {
				task := Task{}

				if errors.Is(cursor.Err(), mongo.ErrNoDocuments) {
					// no documents in cursor, breaking
					break
				}

				if taskErr := safeConvert(cursor, &task); taskErr != nil {
					// we have found broken task, so we must try to change it status to error

					idTask := &IdentifiableTask{}
					if err := safeConvert(cursor, idTask); err != nil {
						// we can't extract id, so just log and skip...
						repository.log.Errorf("failed to decode identifiable task in project '%s': %s: %s", idProject.DocumentID, err, taskErr)
						continue
					}

					continue
				}

				tasks = append(tasks, task)
			}

			return &ProjectResult{Project: &Project{DocumentID: idProject.DocumentID, ID: idProject.ID, Tasks: tasks}}
		}

		return &ProjectResult{Project: project}
	})
}

func (repository *ProjectsRepository) SaveProjectWithTaskStatuses(ctx context.Context, projectID primitive.ObjectID, updated map[internal.TaskID]Task) error {
	tasksUpdates := make([]bson.M, 0, len(updated)*3+1)
	tasksFilters := make([]interface{}, 0, len(updated))
	now := primitive.NewDateTimeFromTime(time.Now())

	tasksUpdates = append(tasksUpdates, bson.M{seenAtProjectProperty: now})

	count := 0
	for id, task := range updated {
		count++
		taskFilter := fmt.Sprintf("task%d", count)
		tasksFilters = append(tasksFilters, bson.M{fmt.Sprintf("%s.task-id", taskFilter): id})

		tasksUpdates = append(tasksUpdates, bson.M{fmt.Sprintf("tasks.$[%s].%s", taskFilter, statusTaskProperty): task.Status})
		tasksUpdates = append(tasksUpdates, bson.M{fmt.Sprintf("tasks.$[%s].%s", taskFilter, statusDetailTaskProperty): task.StatusDetails})
		tasksUpdates = append(tasksUpdates, bson.M{fmt.Sprintf("tasks.$[%s].%s", taskFilter, modifiedAtTaskProperty): now})
	}

	update := merge(set(tasksUpdates...), unset(bson.M{lockedAtProjectProperty: ""}))
	filters := options.Update().SetArrayFilters(options.ArrayFilters{Filters: tasksFilters})

	return repository.updateByID(ctx, projectID, update, filters)
}

func (repository *ProjectsRepository) SaveProjectWithTaskLogStatuses(ctx context.Context, projectID primitive.ObjectID, updated map[internal.TaskID]Task) error {
	if len(updated) == 0 {
		return nil
	}
	tasksUpdates := make([]bson.M, 0, len(updated)+1)
	tasksFilters := make([]interface{}, 0, len(updated))
	now := primitive.NewDateTimeFromTime(time.Now())

	tasksUpdates = append(tasksUpdates, bson.M{loggingSeenAtProjectProperty: now})

	count := 0
	for id, task := range updated {
		count++
		taskFilter := fmt.Sprintf("task%d", count)
		tasksFilters = append(tasksFilters, bson.M{fmt.Sprintf("%s.task-id", taskFilter): id})

		tasksUpdates = append(tasksUpdates, bson.M{fmt.Sprintf("tasks.$[%s].%s", taskFilter, loggingStatusTaskProperty): task.LogStatus})
	}

	update := merge(set(tasksUpdates...), unset(bson.M{loggingLockedAtProjectProperty: ""}))
	filters := options.Update().SetArrayFilters(options.ArrayFilters{Filters: tasksFilters})

	return repository.updateByID(ctx, projectID, update, filters)
}

func (repository *ProjectsRepository) updateByID(ctx context.Context, id primitive.ObjectID, update interface{}, opts ...*options.UpdateOptions) error {
	result, err := repository.collection().UpdateByID(ctx, id, update, opts...)
	if err != nil {
		return fmt.Errorf("failed to save updated document '%s': %w", id, err)
	}

	if result.MatchedCount < 1 {
		return fmt.Errorf("none of documents match id '%s'", id)
	}

	return nil
}

func (repository *ProjectsRepository) createChannel(limit int, block func() *ProjectResult) chan ProjectResult {
	channel := make(chan ProjectResult, limit)

	go func() {
		defer func() {
			recovered := recover()
			if recovered != nil {
				debug.PrintStack()
				repository.log.Errorf("error happend while obtaining projects: %s", recovered)
			}

			close(channel)
		}()

		for {
			result := block()

			if result == nil {
				break
			}

			channel <- *result
		}
	}()

	return channel
}

func (repository *ProjectsRepository) collection() *mongo.Collection {
	return repository.client.Database(repository.properties.Database).Collection(ProjectsCollection)
}

func safeConvert(result decoder, item interface{}) (err error) {
	defer func() {
		recovered := recover()
		if recovered != nil {
			err = fmt.Errorf("recovered from error: %w", err)
		}
	}()

	if err := result.Decode(item); err != nil {
		return fmt.Errorf("failed to decode project: %w", err)
	}

	return nil
}

func merge(items ...bson.M) bson.M {
	result := bson.M{}

	for _, item := range items {
		for property, value := range item {
			if result[property] != nil {
				panic(fmt.Sprintf("failed to merge operations, '%s' is duplicated", property))
			}

			result[property] = value
		}
	}

	return result
}

func unset(items ...bson.M) bson.M {
	return bson.M{"$unset": merge(items...)}
}

func set(items ...bson.M) bson.M {
	return bson.M{"$set": merge(items...)}
}

func or(items ...bson.M) bson.M {
	result := bson.A{}

	for _, item := range items {
		result = append(result, item)
	}

	return bson.M{"$or": result}
}

func and(items ...bson.M) bson.M {
	result := bson.A{}

	for _, item := range items {
		result = append(result, item)
	}

	return bson.M{"$and": result}
}
